﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Reflection;
using System.Text;

using LostLib.Linq;
using LostLib.Collections.Generic;

namespace NTypeClasses
{
	static class TypeClassDynamicCode{
		static TypeClassDynamicCode()
		{
			var assemblyName = new AssemblyName("NTypeClasses.DynamicCode");
			var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
			dynamicModule = assembly.DefineDynamicModule(assemblyName.Name);
		}

		internal static readonly ModuleBuilder dynamicModule;
	}

	/// <summary>
	/// A class to work with specified type class's element.
	/// </summary>
	/// <typeparam name="I">An element from some type class</typeparam>
	public class TypeClass<I> where I: class
	{
		static readonly I defaultImplementation;
		static TypeClass()
		{
			defaultImplementation = createDefaultImplementation();
		}

		/// <summary>
		/// An instance that implements all operations defined in a type class for I.
		/// </summary>
		public static I Instance
		{
			get
			{
				if (defaultImplementation == null) {
					// defaultImplementation = createDefaultImplementation();
				}

				return defaultImplementation;
			}
		}

		static readonly ModuleBuilder dynamicModule = TypeClassDynamicCode.dynamicModule;

		static string BeforeAccent(string str)
		{
			int accent = str.IndexOf('`');
			if (accent < 0) return str;
			return str.Substring(0, accent);
		}

		static string GenerateTypeName()
		{
			var result = new StringBuilder();
			result.Append(BeforeAccent(typeof (I).Name));
			result.Append('<');


			var typarDAG = new DirectedAcyclicGraph<int, HashSet<int>>();
			var typeParams = typeof (I).GetGenericTypeDefinition().GetGenericArguments();
			var typarIndexMap = Enumerable.Repeat(-1, typeParams.Length).ToArray();
			var typarReverseMap = (int[])typarIndexMap.Clone();
			for (int i = 0; i < typeParams.Length; i++) {
				var param = typeParams[i];
				var typars = param.GetCustomAttributes(typeof (TyparAttribute), false);
				if (typars.Length > 1)
					throw new ArgumentException("Only single Typar attribute allowed");
				if (typars.Length == 0) {
					typarDAG.Add(i, new HashSet<int>());
				} else {
					var typar = (TyparAttribute)typars[0];
					HashSet<int> possibleArgs;
					typarReverseMap[i] = typar.Index;
					if (typarIndexMap[typar.Index] < 0) {
						typarIndexMap[typar.Index] = i;
						possibleArgs = new HashSet<int>();
						typarDAG.Add(i, possibleArgs);
					} else {
						var firstOccurenceIndex = typarIndexMap[typar.Index];
						possibleArgs = typarDAG[firstOccurenceIndex];
					}
					foreach (var argIndex in typar.Args) {
						possibleArgs.Add(argIndex);
					}
				}
			}

			var visited = new bool[typeParams.Length];
			var typeArgs = typeof (I).GetGenericArguments();
			Action<int, bool> visit = null;
			visit = (argIndex, forceRec) => {
				if (visited[argIndex] && !forceRec) return;
				visited[argIndex] = true;
				var arg = typeArgs[argIndex];
				
				var typars = typeParams[argIndex].GetCustomAttributes(typeof (TyparAttribute), false);
				if (typars.Length == 0) {
					result.Append(SimpleName(arg));
					return;
				}

				result.Append(BeforeAccent(arg.Name));
				result.Append('[');
				var typar = (TyparAttribute) typars[0];
				Enumerable.Range(0, typarReverseMap.Length)
					.Where(i => typarReverseMap[i] == typar.Index)
					.ForEach(innerIdx => {
						var innerTypar = (TyparAttribute)typeParams[innerIdx]
							.GetCustomAttributes(typeof (TyparAttribute), false)[0];
						innerTypar.Args.ForEach(
							typarArg => visit(typarArg, true),
							() => result.Append(", "));
					},
					() => result.Append("; "));
				result.Append(']');
			};
			bool firstArg = true;
			var argOrder = typarDAG.TopologicalSort().Reverse().ToArray();
			foreach (var argIndex in argOrder) {
				if (visited[argIndex]) continue;
				if (!firstArg) result.Append(", ");
				firstArg = false;
				visit(argIndex, false);
			}

			result.Append('>');
			result.Append(Guid.NewGuid().ToString());
			return result.ToString();
		}

		private static string SimpleName(Type type)
		{
			var genericArgs = type.GetGenericArguments();
			if (genericArgs.Length == 0) return type.Name;

			var result = new StringBuilder();
			result.Append(BeforeAccent(type.Name));
			result.Append('<');

			genericArgs.ForEach(
				t => result.Append(SimpleName(t)),
				() => result.Append(", "));

			result.Append('>');
			return result.ToString();
		}

		static I createDefaultImplementation()
		{
			var typeName = GenerateTypeName();
			var type = dynamicModule.DefineType(typeName, TypeAttributes.Sealed | TypeAttributes.Class, typeof(I));

			createMethodImplementations(type);
			// createPropertyImplementations(type);

			//try {
				var impl = type.CreateType();
				var instance = Activator.CreateInstance(impl);
				if (instance == null) throw new InvalidProgramException();
				return (I)instance;
			//} catch (Exception e) {
			//    throw e;
			//}
		}

		//private static void createPropertyImplementations(TypeBuilder type)
		//{
		//    foreach (var prop in typeof(I).GetProperties(BindingFlags.Instance | BindingFlags.Public)) {
		//        createMethodImplementation(type, prop.GetGetMethod());
		//        createMethodImplementation(type, prop.GetSetMethod());
		//    }
		//}

		private static void createMethodImplementations(TypeBuilder type)
		{
			foreach (var method in typeof(I).GetMethods(BindingFlags.Instance | BindingFlags.Public)) {
				createMethodImplementation(type, method);
			}
		}

		static void createMethodImplementation(TypeBuilder type, MethodInfo method)
		{
			if (method == null || !method.IsAbstract && !method.IsVirtual) return;

			var methodImpl = TypeClasses.GetMethodImplementation(typeof(I), method);

			if (methodImpl != null) {
				var methodBuilder = Override(type, method);
				var il = methodBuilder.GetILGenerator();
				ushort index = 1;
				foreach (var p in method.GetParameters()) {
					il.Emit(OpCodes.Ldarg, index++);
				}
				var call = methodImpl.IsVirtual && !methodImpl.IsFinal ? OpCodes.Callvirt : OpCodes.Call;
				// il.Emit(OpCodes.Tailcall);
				il.Emit(call, methodImpl);
				il.Emit(OpCodes.Ret);
			} else {
				if (method.IsAbstract) throw new InvalidProgramException();
			}
		}

		private static MethodBuilder Override(TypeBuilder type, MethodInfo method)
		{
			return type.DefineMethod(method.Name, MethodAttributes.Public | MethodAttributes.ReuseSlot |
				MethodAttributes.HideBySig | MethodAttributes.Virtual, method.ReturnType,
				method.GetParameters().Select(p => p.ParameterType).ToArray());
		}
	}
}
